Podstawową funkcjonalnością naszego bloga ma być wyświetlanie artykułu po kliknięciu w jego tytuł w lewej kolumnie. Skorzystamy tutaj z rozwiązań, które poznaliśmy już w poprzednim module. Zacznijmy jednak od doprecyzowania działania tej funkcjonalności.
Na razie skupimy się tylko na kliknięciu w link, który został dodany w pliku index.html – dopiero później zastanowimy się, w jaki sposób generować listę linków. Przejdźmy więc do algorytmu naszej pierwszej funkcjonalności!
Algorytm działania skryptu
Wiemy już, że możemy powiązać kliknięcie w guzik (lub – w tym wypadku – link) z wykonaniem funkcji. Skąd jednak mamy wiedzieć, że dany link ma wyświetlić ten konkretny artykuł? Jak pamiętasz z modułu o Bootstrapie, skrypt musi "wiedzieć", że jeden element ma wpływać na drugi. Musimy je jakoś ze sobą powiązać!
Spójrz na kod HTML naszego bloga. Lista tytułów składa się z linków, których atrybuty href zostały wypełnione. Bliższa analiza kodu pokazuje, że wartości tych atrybutów to symbol #, po którym podane jest id powiązanego artykułu. Za chwilę wykorzystamy tę zbieżność w naszym kodzie!
Skoro już wiemy, w jaki sposób link z listy tytułów jest powiązany z artykułem, to w jaki sposób będziemy wyświetlać i ukrywać artykuły? Jeżeli przyjrzysz się SCSS-owi z naszego szablonu, zauważysz, że domyślnie wszystkie artykuły na blogu (elementy o klasie .post) są ukryte za pomocą deklaracji display: none;. Wyświetlane są tylko artykuły, które posiadają dodatkowo klasę active, która nadaje elementowi właściwość display: block;. Możemy zatem za pomocą JS dynamicznie nadawać i usuwać klasę active, aby pokazywać lub ukrywać artykuł.
Zostaje jeszcze jedna ważna kwestia: chcielibyśmy, żeby kliknięty link zmieniał kolor. W ten sposób oznaczymy na liście tytuł aktualnie wyświetlanego artykułu. W tym przypadku również wykorzystamy klasę active.
W takim razie algorytm działania skryptu będzie taki:
- po kliknięciu linka:
- ustaw klasy linków:
- usuń klasę
active ze wszystkich linków na liście tytułów,
- dodaj klasę
active do klikniętego linka,
- ukryj wszystkie artykuły:
- usuń klasę
active ze wszystkich artykułów,
- znajdź artykuł do wyświetlenia:
- z klikniętego linka weź zawartość atrybutu
href, np. #article-2,
- znajdź na stronie element pasujący do selektora takiego, jak wartość atrybutu
href, np. #article-2 – czyli szukamy elementu o id="article-2",
- wyświetl znaleziony artykuł:
- dodaj klasę
active do znalezionego artykułu.
Z tak rozpisanym algorytmem jesteśmy prawie gotowi do pisania skryptu! Prawie, bo jeszcze nie wszystko z tej listy umiemy zrobić – ale będziemy to tłumaczyć na bieżąco.
Zdarzenia, czyli eventy
Podobnie jak w poprzednim module, napiszemy funkcję, która będzie działać po kliknięciu elementu na stronie. Zanim do tego przejdziemy, warto wspomnieć, że kliknięcie jest zdarzeniem, czyli eventem.
W JavaScripcie spotkasz się z wieloma różnymi eventami. Możesz nawet tworzyć własne i wykorzystywać do budowania lepszej architektury skryptu. Zacznijmy jednak od krótkiego wyjaśnienia, czym są eventy.
Metafora — event
Event możesz wyobrazić sobie jako zdarzenie, które każdy może zaobserwować. Przykładem może być zapalenie się zielonego światła na skrzyżowaniu.
To samo zdarzenie będzie miało różne skutki dla różnych osób:
- piesi w ogóle nie patrzą na sygnalizator dla samochodów, więc w żaden sposób nie zareagują na zapalenie się zielonego światła,
- kierowcy jadący na wprost ruszą i przejadą przez skrzyżowanie,
- kierowcy skręcający w prawo będą mogli skręcić, ale muszą najpierw udzielić pierwszeństwa pieszym przechodzącym przez jezdnię, w którą chcą skręcić,
- kierowca karetki jadącej na sygnale nie stosuje się do sygnalizacji, ale zielone światło do jazdy na wprost mówi mu, że nie musi się obawiać samochodu wjeżdżającego na skrzyżowanie z innego kierunku.
Zwróć uwagę, że nie każdy na skrzyżowaniu patrzy na to światło, i nie każdy, kto je obserwuje, zareaguje w ten sam sposób.
W JavaScripcie, do każdego elementu na stronie, można dodać "nasłuchiwacz zdarzeń". Brzmi to dużo zgrabniej po angielsku – event listener. Będzie on czekać na konkretny event występujący na tym elemencie. Kiedy się doczeka, wykona wcześniej zdefiniowaną funkcję, której przekaże informacje o tym evencie.
Rozpoczynając pracę z eventami, warto wspomnieć o kluczowej zasadzie ich działania. Zastanów się nad taką sytuacją: mamy link, w którym umieściliśmy obrazek. Czy kliknięcie obrazka oznacza też kliknięcie linka? Musi tak być, bo inaczej musielibyśmy przypisać funkcję do kliknięcia każdego elementu w linku!
W JS zostało to rozwiązane za pomocą bąbelkowania, czy bardziej profesjonalnie – propagacji. W podanym przykładzie obrazek otrzyma event click. Następnie przekaże ten event swojemu rodzicowi – linkowi. Dzięki temu link też będzie mógł zareagować na kliknięcie. Link również przekaże ten event swojemu rodzicowi – i tak event będzie bąbelkować do samej góry, aż do body, document i wreszcie window.
Funkcja addEventListener
W poprzednim module mieliśmy już do czynienia z tą funkcją przy obsługiwaniu kliknięcia guzika:
document.getElementById('test-button').addEventListener('click', function(){
console.log('Guzik został kliknięty');
});
Funkcja czy metoda?
W tym module używamy określenia "funkcja", aby nie komplikować sytuacji. W następnym module dowiesz się, że kiedy funkcję wykonujemy po kropce, to nazywa się ona metodą. Technicznie rzecz biorąc, każda metoda jest funkcją, tak jak każdy kwadrat jest jednocześnie prostokątem, jednak w praktyce używa się dokładniejszego określenia – metoda. Wspominamy o tym teraz, aby nie zdziwiło Cię, kiedy spotkasz się z tym określeniem.
Przeanalizujmy ostatnią linię powyższego kodu:
document.getElementById('test-button') znajduje na stronie element o id="test-button"),
.addEventListener jest wybraniem funkcji addEventListener przypisanej do tego guzika – czyli chcemy powiązać jakąś funkcję z jakimś eventem występującym na tym elemencie,
( ) – te nawiasy, umieszczone bezpośrednio po nazwie funkcji, mówią nam, że wykonujemy tę funkcję,
'click' to rodzaj eventu, którego będziemy nasłuchiwać,
function rozpoczyna anonimową funkcję, która ma zostać wykonana po wystąpieniu tego eventu na tym elemencie.
Przy okazji warto wspomnieć, że funkcja wykonywana w reakcji na event jest często nazywana handlerem. Innymi słowy, handler eventu zostanie wykonany przez listener w momencie wykrycia eventu danego typu.
Jak znaleźć wszystkie linki?
Wiemy już sporo o eventach i ich listenerach, więc czas zastosować tę wiedzę do linków na liście tytułów. Moglibyśmy każdemu z nich nadać id i wykorzystać powyższy fragment kodu, ale musielibyśmy to zrobić wiele razy – i dodawać kolejne fragmenty kodu JS przy każdym dodaniu nowego artykułu.
Jak pewnie się domyślasz, jest na to lepszy sposób. Wykorzystamy jedną z dwóch funkcji, która pozwala na znalezienie elementu za pomocą selektora CSS! To się świetnie składa, bo znasz już działanie selektorów!
Mamy do dyspozycji dwie funkcje:
document.querySelector(selector), która wyszuka pierwszy pasujący element,
document.querySelectorAll(selector), która wyszuka wszystkie elementy, pasujące do selektora.
Będzie nas interesować druga metoda, więc to właśnie ją zastosujemy w docelowym skrypcie. Przećwiczmy ją teraz i wykorzystajmy wcześniejszy kod do wyświetlenia w konsoli listy linków po kliknięciu w guzik:
- W swoim pliku
index.html, w dowolnym miejscu (np. w lewym sidebarze nad listą linków) dodaj element button z atrybutem id="test-button".
- W pliku
js/script.js wklej poniższy kod:
document.getElementById('test-button').addEventListener('click', function(){
const links = document.querySelectorAll('.titles a');
console.log('links:', links);
});
- Otwórz konsolę w narzędziach developerskich i sprawdź, co się w niej pojawi po kliknięciu guzika, który przed chwilą dodaliśmy do strony.
Jak widzisz w konsoli, po kliknięciu guzika funkcja querySelectorAll znalazła wszystkie elementy pasujące do selektora .titles a. Koniecznie w konsoli kliknij trójkąt, który pojawił się przy wyświetlonym komunikacie. W ten sposób zobaczysz pełną zawartość stałej links!
Jeżeli udało Ci się uzyskać taki sam efekt – znakomicie! Twój kod działa, jak należy. Możesz teraz skasować button z pliku index.html oraz zakomentuj kod, który wstawiliśmy do pliku js/script.js.
Wiemy już, jak wyszukać wszystkie linki. Czy zatem teraz wystarczy napisać tylko links.addEventListener(...)? Niestety, nie będzie tak łatwo. Funkcji addEventListener możemy użyć tylko na pojedynczym elemencie. W takim razie musimy napisać pętlę...
Pętla for-of
Jeśli pamiętasz jeszcze rozdział o algorytmach z poprzedniego modułu, pokazaliśmy w nim zagadnienie pętli. Teraz poznamy pętle w JS.
Pętla to nic innego jak wykonywanie tych samych czynności w kółko. Każde wykonanie tych operacji, czyli każdy "obrót" pętli, nazywamy iteracją.
Metafora — pętla
Załóżmy, że Twój kot często chowa się w pudełku, kiedy chcesz go zabrać do weterynarza. Potrzebujesz w takim razie znaleźć kota, a masz przed sobą wiele pudełek.
Dla każdego pudełka musisz wykonać następujący algorytm:
- jeśli pudełko jest za małe dla kota, przejść do kolejnego pudełka,
- jeśli pudełko jest oklejone taśmą klejącą i kot nie mógł do niego wejść, przejść do kolejnego pudełka,
- otwórz pudełko, i jeśli nie ma w nim kota, przejść do kolejnego pudełka,
- jeśli jest w nim kot, wyjmij kota i nie sprawdzać kolejnych pudełek.
W przypadku naszego skryptu, mamy stałą links, która zawiera jakąś liczbę linków. Pisząc nasz skrypt, nie wiemy ile linków będzie – i nie potrzebujemy tego wiedzieć. Pętla ma coś zrobić dla każdego z nich. Składnia pętli w naszym skrypcie będzie wyglądała następująco:
const links = document.querySelectorAll('.titles a');
for(let link of links){
console.log(link);
}
Powyższy przykład po prostu wyświetli w konsoli każdy element, zawarty w links.
Przeanalizujmy składnię tej pętli:
for – to będzie pętla,
( ) – w tych nawiasach zawieramy definicję działania pętli,
let link – deklarujemy zmienną, w której znajdzie się pojedynczy element z links,
of – łącznik, który wyjaśnimy w następnym module,
links – kolekcja, z której mają być brane elementy, po których iterujemy pętlę,
{ } – w tych nawiasach wpisujemy operacje, które mają być wykonane dla każdego iterowanego elementu.
Zobaczmy więc, w jaki sposób możemy wykorzystać pętlę do przypisania event listenerów do każdego linka. Wstaw w swoim pliku js/script.js poniższy kod, i obserwuj w konsoli developerskiej, co się dzieje, gdy klikasz linki na liście tytułów:
const titleClickHandler = function(){
console.log('Link was clicked!');
}
const links = document.querySelectorAll('.titles a');
for(let link of links){
link.addEventListener('click', titleClickHandler);
}
Zwróć uwagę, że tym razem zapisaliśmy handler eventu w zmiennej titleClickHandler – za chwilę zacznie się on rozrastać, więc taki zapis będzie dla nas wygodniejszy, niż zastosowanie anonimowej funkcji bezpośrednio jako drugi argument funkcji addEventListener (wewnątrz pętli).
No dobrze, zaglądamy do konsoli i coś się tam wyświetla, ale... co właściwie chcieliśmy w ten sposób osiągnąć?
W trakcie pracy nad skryptem łatwo jest zapomnieć, jaki był algorytm. Dlatego warto przenieść go sobie bezpośrednio do kodu, który piszemy! Wykorzystamy do tego komentarze – oczywiście, zgodnie z dobrymi praktykami, napiszemy je po angielsku.
Doprowadź swój plik js/script.js do stanu pokazanego poniżej:
const titleClickHandler = function(event){
console.log('Link was clicked!');
}
const links = document.querySelectorAll('.titles a');
for(let link of links){
link.addEventListener('click', titleClickHandler);
}
Tym razem wykorzystaliśmy inny rodzaj komentarza – komentarz blokowy. W przeciwieństwie do komentarza rozpoczynającego się od //, ten rodzaj komentarza może mieć wiele linii. Tym razem jednak użyliśmy go tylko po to, aby wyglądał inaczej niż komentarze //, których będziemy używać do zamieszczania innych uwag.
To świetna metoda na ułatwienie sobie pracy. Od teraz będziemy wypełniać kod pod każdym komentarzem. Przy okazji od razu zrobimy sobie dokumentację naszego skryptu.
Ćwiczenie
Każdy handler eventu będzie otrzymywał argument, zawierający informacje na temat wychwyconego zdarzenia. Standardowo temu argumentowi nadaje się nazwę event, co też zrobiliśmy w powyższym przykładzie.
Do funkcji titleClickHandler dodaj console.log, który wyświetli zawartość argumentu event. Obejrzyj w konsoli jego zawartość.
Jak widzisz, jest w nim mnóstwo informacji i funkcji. Jedną z ciekawszych jest target, które pokazuje element, który został kliknięty. W naszym przykładzie, w każdym linku znajduje się <span>, a dopiero w nim tekst. Dlatego w argumencie event jako target podany jest span.
Dodaj kod z powyższego przykładu (razem z console.log dodanym w tym ćwiczeniu) do swojego pliku js/script.js. Zapisz commit i wyślij na zdalne repozytorium.
Zmiana klas elementu na stronie
Pierwszy fragment kodu do napisania w funkcji titleClickHandler to usunięcie klasy active ze wszystkich linków. Musimy w takim razie:
- znaleźć wszystkie linki z klasą
active,
- zastosować pętlę, aby dla każdego z nich:
Umiesz już znaleźć wszystkie elementy pasujące do selektora oraz napisać pętlę, więc bez trudu zrozumiesz je w poniższym kodzie. Nowością będzie tylko sposób na usunięcie klasy z elementu. W swoim pliku js/script.js, pod linią komentarza /* remove class 'active' from all article links */, dodaj następujący kod:
const activeLinks = document.querySelectorAll('.titles a.active');
for(let activeLink of activeLinks){
activeLink.classList.remove('active');
}
Skupmy się na chwilę na tej linii kodu:
activeLink.classList.remove('active');
activeLink to pojedynczy link, spośród wyszukanych linków z klasą active,
.classList to "biblioteka", zawierająca informacje i funkcje dotyczące klas tego elementu,
.remove to jedna z tych funkcji, służąca do usunięcia klasy,
( ) – te nawiasy, umieszczone bezpośrednio po nazwie funkcji, mówią nam, że wykonujemy funkcję,
'active' to klasa, którą chcemy usunąć.
Inne przydatne funkcje w classList, których możemy użyć analogicznie do remove, to:
add – dodawanie klasy,
toggle – "przełączanie" klasy, czyli "jeśli element nie ma podanej klasy, to ją dodaj – w przeciwnym wypadku usuń tę klasę z tego elementu",
contains – sprawdzenie, czy element posiada daną klasę.
Świetnie – usuwanie aktywnej klasy już działa. Możesz to sprawdzić, dodając któremuś linkowi klasę active w kodzie HTML. Po kliknięciu któregokolwiek linka klasa zostanie usunięta.
Ćwiczenie
Mamy już gotowy fragment funkcji titleClickHandler, zaczynający się od komentarza:
Poniżej znajduje się bardzo podobny komentarz:
Uzupełnij ten fragment funkcjonalności, wykorzystując bardzo podobny kod – musisz wprowadzić tylko dwie zmiany:
- zmień nazwy stałych i zmiennych z
activeLinks i activeLink na activeArticles i activeArticle,
- zmień selektor
.titles a.active na odpowiedni dla artykułów znajdujących się w naszym blogu, które posiadają klasę active.
W efekcie tych zmian, po kliknięciu któregokolwiek linka w lewej kolumnie, artykuł w środkowej kolumnie powinien zniknąć. Nie pojawi się na jego miejsce żaden inny, ponieważ jeszcze nie nadajemy klasy active na żaden artykuł.
Nie zapomnij zapisać commita i wysłać go na zdalne repozytorium.
"Magiczne" słowo this
Kolejnym krokiem jest nadanie klasy active dla klikniętego linka. Domyślasz się pewnie, że pomoże nam w tym owo "magiczne" słowo this. Zobaczmy, jak zmieni się nasz kod, a później wyjaśnimy, dlaczego w ten sposób użyliśmy w nim this.
W komentarzach, za pomocą [DONE] oznaczyliśmy fragmenty funkcji titleClickHandler, które są już gotowe, lub zostały wykonane przez Ciebie w ramach poprzedniego ćwiczenia. Fragment, nad którym pracujemy teraz, oznaczyliśmy jako [IN PROGRESS].
Zastosuj teraz taką samą konwencję w swoim pliku js/script.js.
const titleClickHandler = function(event){
const clickedElement = this;
console.log('Link was clicked!');
const activeLinks = document.querySelectorAll('.titles a.active');
for(let activeLink of activeLinks){
activeLink.classList.remove('active');
}
console.log('clickedElement:', clickedElement);
}
const links = document.querySelectorAll('.titles a');
for(let link of links){
link.addEventListener('click', titleClickHandler);
}
Wewnątrz funkcji titleClickHandler mamy do dyspozycji dwa źródła informacji o evencie, czyli w naszym przypadku – o kliknięciu:
- argument funkcji, który nazwaliśmy
event,
- obiekt
this.
Wcześniej już sprawdzaliśmy, jakie informacje znajdują się w argumencie event. Znaleźliśmy w nim m.in. informację target, która jednak w naszym przypadku zawierała odniesienie do spana, znajdującego się w linku. Moglibyśmy jednak skorzystać z currentTarget zamiast target – tam znaleźlibyśmy odniesienie do elementu, do którego dodaliśmy listener. Ta sama informacja znajdzie się jednak w obiekcie this, a jego znajomość jest bardzo ważna, więc to na nim się skupimy.
Słowo this określiliśmy jako "magiczne", ponieważ – szczególnie na początku nauki JS – wydaje się nieco tajemnicze. Wynika to z faktu, że zmienia ono znaczenie, w zależności od tego, gdzie zostało użyte. Dlatego przyjmiemy zasadę, że dla unikania błędów i zwiększenia czytelności kodu, będziemy go używać jak najrzadziej – zapiszemy jego wartość do stałej, która będzie miała bardziej przyjazną nazwę.
W przypadku handlera eventu click, pierwszą linijką funkcji niech zawsze będzie:
const clickedElement = this;
Dzięki temu w całej funkcji będziemy mogli używać clickedElement, bez zastanawiania się, co w tym miejscu oznacza this.
Przy okazji zauważ, że tym razem inaczej użyliśmy console.log. Zamiast łączyć teksty za pomocą znaku +, podaliśmy dwa argumenty, rozdzielając je przecinkiem. Dzięki temu zobaczymy znacznie więcej informacji o sprawdzanej zmiennej (lub stałej, argumencie czy obiekcie). Sprawdź jak zmieni się komunikat w konsoli, jeśli użyjesz np. console.log('clickedElement (with plus): ' + clickedElement);.
Ćwiczenie
Mamy już odniesienie do klikniętego linka, zapisane w stałej clickedElement. Twoim zadaniem jest dodanie mu klasy active.
Pamiętaj, że nie potrzebujesz pętli, ponieważ clickedElement jest odniesieniem do pojedynczego elementu, a nie grupy elementów. W takim razie wystarczy jedna linijka kodu, w której zamiast usuwania klasy (remove) będziemy dodawać klasę (add).
Już wcześniej po kliknięciu któregokolwiek linka w lewej kolumnie, aktywny link tracił klasę active. W efekcie tego ćwiczenia, kliknięty link powinien otrzymywać klasę active – dzięki temu ostatnio kliknięty link powinien być podświetlony!
Nie zapomnij zapisać commita i wysłać go na zdalne repozytorium.
Blokowanie domyślnej akcji
Jak dobrze wiesz, kliknięcie linka na stronie zmienia adres strony. Jeśli wartość atrybutu href będzie się zaczynać od #, to zmieni się tylko końcówka adresu. Ten fragment URL-a strony, od znaku # do końca, nazywa się określeniem hash.
Zmiana hasha adresu strony ma domyślne zastosowanie – przewija stronę tak, aby element o takim samym id znajdował się na samej górze okna. Jest to mechanizm pozwalający na proste przewijanie strony do odpowiedniej sekcji. Dla przykładu, spis treści artykułu na Wikipedii działa dokładnie w ten sposób – po kliknięciu tytułu rozdziału w spisie treści zostajemy natychmiast przeniesieni do odpowiedniego rozdziału.
Na naszym blogu nie potrzebujemy tego mechanizmu, a wręcz przeciwnie – byłby on irytujący dla użytkownika. Dlatego skorzystamy z możliwości wyłączenia domyślnego zachowania przeglądarki przy kliknięciu w linki, które obsługujemy za pomocą naszego skryptu JS.
Wystarczy, że w handlerze eventu (w naszym przypadku jest to funkcja titleClickHandler) dodamy linię:
event.preventDefault();
Jak widzisz, implementacja rozwiązania jest dużo prostsza, niż wytłumaczenie tej kwestii. Zwróć uwagę, że wykorzystujemy tutaj argument event, który jest przyjmowany przez handler.
Zwyczajem jest dodawanie tej linii na początku funkcji, więc tak też zrobimy – dodaj ją przed deklaracją stałej clickedElement. Adres strony nie powinien się już zmieniać przy klikaniu w linki w lewej kolumnie.
W ten sam sposób możemy blokować domyślne zachowanie dowolnego eventu – np. częstym zastosowaniem jest blokowanie domyślnego działania formularzy, aby umożliwić sprawdzenie poprawności wypełnienia poszczególnych pól (walidacja), oraz wysłać formularz za pomocą JS-a, czyli bez przeładowania strony. Jednak do tego tematu jeszcze wrócimy. ;)